Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add commutator control based on swing-twist decomposition #2

Merged
merged 5 commits into from
Aug 12, 2024

Conversation

glopesdev
Copy link
Contributor

@glopesdev glopesdev commented Aug 10, 2024

Summary

A common pattern of commutation is to use rotation measurements from a BNO055 or similar device to compute a feedback control signal for stabilizing twist in the tether.

This PR introduces a high-level operator AutoCommutator which provides a reusable solution for this pattern. Additionally, we introduce two operators to allow low-level interface with the commutator, and to compute the feedback signal in isolation. This allows composing custom commutation solutions where the control signal may be combined with manual operation, or to enable fine-grained rate control and logging, as demonstrated in the final example.

Operators

  • TurnMotor: the low-level interface to the motor, operated in units of full turns, in the range [-1, 1]. The input is a sequence of turn values. The operator internally formats and sends to a serial port a sequence of JSON strings used to drive the motor controller.
  • QuaternionTwistController: calculates the feedback control signal to stabilize twisting in the tether about the specified axis from a series of Quaternion rotation measurements. The twist about the specified axis is used to compute the differential feedback signal which is then normalized into units of turns.
  • AutoCommutator: an embedded workflow which samples the rotation measurements at 10 Hz, computes the differential feedback signal and drives the motor at the specified serial port.

Examples

Driving the commutator with a BNO055 from the Onix1 library

image

Logging BNO055 measurement timestamps with each command

image

Fixes #1

@glopesdev glopesdev added the feature New planned feature label Aug 10, 2024
@glopesdev glopesdev requested a review from jonnew August 10, 2024 08:07
@glopesdev glopesdev changed the title Add automatic commutator controller based on swing-twist decomposition of rotations Add auto commutator controller based on swing-twist decomposition of rotations Aug 10, 2024
@glopesdev glopesdev changed the title Add auto commutator controller based on swing-twist decomposition of rotations Add commutator controller based on swing-twist decomposition of rotations Aug 10, 2024
@glopesdev glopesdev changed the title Add commutator controller based on swing-twist decomposition of rotations Add commutator control based on swing-twist decomposition of rotations Aug 10, 2024
@glopesdev glopesdev changed the title Add commutator control based on swing-twist decomposition of rotations Add commutator control based on swing-twist decomposition Aug 10, 2024
@jonnew
Copy link
Member

jonnew commented Aug 11, 2024

Its funny -- I think that if we are going to make these elements composable, then we are in an unhappy medium here. The original meganode on the issue-1 branch was an attempt at dead simplicity for the user. The AutoCommutator workflow in this PR provides a similar level of useability, so that's covered. However, the TurnMotor sink feels kind of unnecessary to me. Right now it does three things

  1. Provide access to write to a serial port. WriteSerialLine does this pretty effectively already
  2. Formats a string. Expressions.Format seems like its fine for this already
  3. Toss ridiculous turn commands (NaN or inifnity). I guess a custom expression script would be needed to do this filtering in Bonsai, so that's really the only major thing I see.

Aside from the limited gains from this node, what if we want more control over the Commuator state beyond turning the motor? e.g. what if we wanted to have some Property that provides control over the commutator's LED state using {led: false}. We could

  1. Make a property in TurnMotor that modifies the string sent to the motor to include the the additional LED state (non-reactive since it changes when the turn command issued)
  2. Make a subject in TurnMotor that merges its output sequence with turn commands
  3. We could add a parallel stream in the AutoCommutator workflow that sent {led: false} to a SerialWriteLine with the same port as the TurnMotor. This feels hacky because PortName needs to be defined for both the TurnMotor node and the SerialWriteLine node.

These all feel kinda hacky in comparison to defining AutoCommuator as the following:

image

So my suggestion is to remove TurnMotor. If customizable control over the behavior of the commuator is what making composable nodes provides, then I think TurnMotor actually hurts in this regard.

@glopesdev
Copy link
Contributor Author

@jonnew Thanks for clarifying the additional functionality of the commutator device. Given this I agree we should refactor the TurnMotor operator to be either a CommutatorDevice or simply Commutator and expose a more general interface to the entire device functionality.

Detailed reasons below:

Formats a string. Expressions.Format seems like its fine for this already

While it is possible to format the JSON string with the Format operator, this locks us to a specific communication protocol. If we ever need to upgrade or extend the details of the JSON API in any way (or swap the protocol to binary, etc), we would not be able to easily upgrade old workflows. With the operator abstracting away the communication internals, we can get away with just upgrading the package, even for hand-crafted workflows.

I don't currently see any particular advantage of exposing the internal JSON strings, especially given the protocol does not comply to an existing standard and there is little cost or complexity added in doing this abstraction.

Toss ridiculous turn commands (NaN or inifnity). I guess a custom expression script would be needed to do this filtering in Bonsai, so that's really the only major thing I see.

Input validation is definitely an important consideration. Similar to the above, having the packaged node would make it much easier to upgrade legacy workflows to benefit from improved validation. For example, in the future we could extend this operator to drop or throw if inputs are sent too fast, etc.

Aside from the limited gains from this node, what if we want more control over the Commuator state beyond turning the motor?

This is where having a general CommutatorDevice node would really make this interface shine and again allow us to extend the device or API with extra functionality by simply updating the package.

The easiest way to do this would have been for the commutator to use one of the standard control protocols already integrated into bonsai such as Harp or even ONI itself. This would have allowed us to leverage a bunch of existing infrastructure for configuration, control, logging and synchronization with other devices. In fact, if it was Harp we could even have generated the entire Bonsai interface automatically.

All the questions you asked about how to dynamically parameterize device configuration and having multiple control streams have already been solved for all Harp devices for example. I don't know what microcontroller the commutator uses, but there is now an existing Harp core for the RP2040 (https://github.com/AllenNeuralDynamics/harp.core.rp2040) which would have been perfect for this. A simple version could also be designed on top of other microcontroller architectures, including software forms of clock synchronization which have been implemented for other devices.

Assuming this is not possible for the current hardware / firmware combination:

Make a property in TurnMotor that modifies the string sent to the motor to include the the additional LED state (non-reactive since it changes when the turn command issued)

This is definitely a possibility and I think we could make this pattern work by using a BehaviorSubject in the backend. Whether this is appropriate depends on how big is the full API, and on whether a simple LED toggle is really the only extra functionality. If there are other configuration or control commands which would be useful to expose as streams (now or in the future) this might quickly become too complicated to evolve.

Make a subject in TurnMotor that merges its output sequence with turn commands

I'm not sure I understood this suggestion correctly, but I'm interpreting this to be the same pattern we use for the Harp Device node, which is basically a node which takes a sequence of commands and formats / pipes them to the serial port.

I think this would definitely be flexible enough for all present and future needs. Basically in this pattern we need just two concepts:

  • A set of commutator nodes (TurnMotor, EnableLed, etc), which format inputs into the corresponding serializable JSON strings (or other protocol) to send to the serial port.
  • A CommutatorDevice node which establishes a connection to the commutator and receives formatted strings.
  • (optional) Make the "command" nodes output an abstract base class instead of strings, so that the CommutatorDevice node can declare a "strongly-typed" input. This would immediately disallow accidentally sending random or malformed strings to the device.

I think the last two bullet points would on their own also justify having a CommutatorDevice node.

Even though SerialWriteLine may suffice to send arbitrary strings to the serial port, this operator does not make any assumptions on port configuration properties such as baud rate, etc. Even if the commutator uses the default settings of the bonsai node for now, there is no guarantee this won't change in the future, either on the side of the commutator, or the side of bonsai, so relying on this alignment to be stable in the future is dangerous IMO. Having a common device node would ensure the alignment is enforced and the port configured correctly.

The last point is optional but easy to implement and important for validation to ensure the commutator does not go into an undefined state by accidentally receiving gibberish data from upstream.

We could add a parallel stream in the AutoCommutator workflow that sent {led: false} to a SerialWriteLine with the same port as the TurnMotor. This feels hacky because PortName needs to be defined for both the TurnMotor node and the SerialWriteLine node.

This could actually be done very easily by linking both properties via the same ExternalizedMapping. However, I do agree that it is still hacky because of all the issues discussed above.

These all feel kinda hacky in comparison to defining AutoCommuator as the following:

Seeing the manual control of the motor with keys and LED control functionality added to the AutoCommutator workflow actually makes me feel that what we should really remove is the AutoCommutator node itself.

It feels there are currently two things being conflated in this package:

  1. The commutator device is actually a complex device, so we need all of its functionality to be exposed in a structured way. This is so we can freely compose what we want to do with it in arbitrary ways, e.g. reset commutator on trials, blink LED to signal something, apply a slower drift correction algorithm, untwist commutator with computer vision tracking, etc the list will just keep growing.

  2. There is a need for a "plug-and-play" commutator solution that users can drop into their acquisition workflows to get started quickly doing some recordings.

I feel that 2. could just as easily be provided simply as an example in the commutator docs, with no need for an embedded workflow. This recommended example usage workflow could be updated regularly as we improve the pattern without requiring updates to the commutator package.

That said, I don't disagree we could also make it into an embedded workflow in the package, although in that case I feel it should be fairly minimalistic, e.g. without assuming specific keys or options, since the more we assume the higher is the risk that it will not work for someone's specific workflow.

- Minor changes to description text
- Rename QuaternionTwistController -> QuaternionToTwist because it does
  not control anything
@jonnew jonnew merged commit 0d14908 into main Aug 12, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New planned feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Description of basic functionality
2 participants